// json5.js
// JSON for Humans. See README.md for details.
//
// This file is based directly off of Douglas Crockford's json_parse.js:
// https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js

var JSON5 = (typeof exports === 'object' ? exports : {});

JSON5.parse = (function () {
	"use strict";

// This is a function that can parse a JSON5 text, producing a JavaScript
// data structure. It is a simple, recursive descent parser. It does not use
// eval or regular expressions, so it can be used as a model for implementing
// a JSON5 parser in other languages.

// We are defining the function inside of another function to avoid creating
// global variables.

	var at,           // The index of the current character
		lineNumber,   // The current line number
		columnNumber, // The current column number
		ch,           // The current character
		escapee = {
			"'":  "'",
			'"':  '"',
			'\\': '\\',
			'/':  '/',
			'\n': '',       // Replace escaped newlines in strings w/ empty string
			b:    '\b',
			f:    '\f',
			n:    '\n',
			r:    '\r',
			t:    '\t'
		},
		ws = [
			' ',
			'\t',
			'\r',
			'\n',
			'\v',
			'\f',
			'\xA0',
			'\uFEFF'
		],
		text,

		renderChar = function (chr) {
			return chr === '' ? 'EOF' : "'" + chr + "'";
		},

		error = function (m) {

// Call error when something is wrong.

			var error = new SyntaxError();
			// beginning of message suffix to agree with that provided by Gecko - see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
			error.message = m + " at line " + lineNumber + " column " + columnNumber + " of the JSON5 data. Still to read: " + JSON.stringify(text.substring(at - 1, at + 19));
			error.at = at;
			// These two property names have been chosen to agree with the ones in Gecko, the only popular
			// environment which seems to supply this info on JSON.parse
			error.lineNumber = lineNumber;
			error.columnNumber = columnNumber;
			throw error;
		},

		next = function (c) {

// If a c parameter is provided, verify that it matches the current character.

			if (c && c !== ch) {
				error("Expected " + renderChar(c) + " instead of " + renderChar(ch));
			}

// Get the next character. When there are no more characters,
// return the empty string.

			ch = text.charAt(at);
			at++;
			columnNumber++;
			if (ch === '\n' || ch === '\r' && peek() !== '\n') {
				lineNumber++;
				columnNumber = 0;
			}
			return ch;
		},

		peek = function () {

// Get the next character without consuming it or
// assigning it to the ch varaible.

			return text.charAt(at);
		},

		identifier = function () {

// Parse an identifier. Normally, reserved words are disallowed here, but we
// only use this for unquoted object keys, where reserved words are allowed,
// so we don't check for those here. References:
// - http://es5.github.com/#x7.6
// - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables
// - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
// TODO Identifiers can have Unicode "letters" in them; add support for those.

			var key = ch;

			// Identifiers must start with a letter, _ or $.
			if ((ch !== '_' && ch !== '$') &&
					(ch < 'a' || ch > 'z') &&
					(ch < 'A' || ch > 'Z')) {
				error("Bad identifier as unquoted key");
			}

			// Subsequent characters can contain digits.
			while (next() && (
					ch === '_' || ch === '$' ||
					(ch >= 'a' && ch <= 'z') ||
					(ch >= 'A' && ch <= 'Z') ||
					(ch >= '0' && ch <= '9'))) {
				key += ch;
			}

			return key;
		},

		number = function () {

// Parse a number value.

			var number,
				sign = '',
				string = '',
				base = 10;

			if (ch === '-' || ch === '+') {
				sign = ch;
				next(ch);
			}

			// support for Infinity (could tweak to allow other words):
			if (ch === 'I') {
				number = word();
				if (typeof number !== 'number' || isNaN(number)) {
					error('Unexpected word for number');
				}
				return (sign === '-') ? -number : number;
			}

			// support for NaN
			if (ch === 'N' ) {
			  number = word();
			  if (!isNaN(number)) {
				error('expected word to be NaN');
			  }
			  // ignore sign as -NaN also is NaN
			  return number;
			}

			if (ch === '0') {
				string += ch;
				next();
				if (ch === 'x' || ch === 'X') {
					string += ch;
					next();
					base = 16;
				} else if (ch >= '0' && ch <= '9') {
					error('Octal literal');
				}
			}

			switch (base) {
			case 10:
				while (ch >= '0' && ch <= '9' ) {
					string += ch;
					next();
				}
				if (ch === '.') {
					string += '.';
					while (next() && ch >= '0' && ch <= '9') {
						string += ch;
					}
				}
				if (ch === 'e' || ch === 'E') {
					string += ch;
					next();
					if (ch === '-' || ch === '+') {
						string += ch;
						next();
					}
					while (ch >= '0' && ch <= '9') {
						string += ch;
						next();
					}
				}
				break;
			case 16:
				while (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f') {
					string += ch;
					next();
				}
				break;
			}

			if(sign === '-') {
				number = -string;
			} else {
				number = +string;
			}

			number = new Number(string);
			Object.defineProperty(number, 'toString', {
				enumerable: false,
				configurable: false,
				writable: false,
				value : function () {
					return sign + string;
				}
			});

			if (!isFinite(number)) {
				error("Bad number");
			} else {
				return number;
			}
		},

		string = function () {

// Parse a string value.

			var hex,
				i,
				string = '',
				delim,      // double quote or single quote
				uffff;

// When parsing for string values, we must look for ' or " and \ characters.

			if (ch === '"' || ch === "'") {
				delim = ch;
				while (next()) {
					if (ch === delim) {
						next();
						return string;
					} else if (ch === '\\') {
						next();
						if (ch === 'u') {
							uffff = 0;
							for (i = 0; i < 4; i += 1) {
								hex = parseInt(next(), 16);
								if (!isFinite(hex)) {
									break;
								}
								uffff = uffff * 16 + hex;
							}
							string += String.fromCharCode(uffff);
						} else if (ch === '\r') {
							if (peek() === '\n') {
								next();
							}
						} else if (typeof escapee[ch] === 'string') {
							string += escapee[ch];
						} else {
							break;
						}
					} else if (ch === '\n') {
						// unescaped newlines are invalid; see:
						// https://github.com/json5/json5/issues/24
						// TODO this feels special-cased; are there other
						// invalid unescaped chars?
						break;
					} else {
						string += ch;
					}
				}
			}
			error("Bad string");
		},

		inlineComment = function () {

// Skip an inline comment, assuming this is one. The current character should
// be the second / character in the // pair that begins this inline comment.
// To finish the inline comment, we look for a newline or the end of the text.

			if (ch !== '/') {
				error("Not an inline comment");
			}

			do {
				next();
				if (ch === '\n' || ch === '\r') {
					next();
					return;
				}
			} while (ch);
		},

		blockComment = function () {

// Skip a block comment, assuming this is one. The current character should be
// the * character in the /* pair that begins this block comment.
// To finish the block comment, we look for an ending */ pair of characters,
// but we also watch for the end of text before the comment is terminated.

			if (ch !== '*') {
				error("Not a block comment");
			}

			do {
				next();
				while (ch === '*') {
					next('*');
					if (ch === '/') {
						next('/');
						return;
					}
				}
			} while (ch);

			error("Unterminated block comment");
		},

		comment = function () {

// Skip a comment, whether inline or block-level, assuming this is one.
// Comments always begin with a / character.

			if (ch !== '/') {
				error("Not a comment");
			}

			next('/');

			if (ch === '/') {
				inlineComment();
			} else if (ch === '*') {
				blockComment();
			} else {
				error("Unrecognized comment");
			}
		},

		white = function () {

// Skip whitespace and comments.
// Note that we're detecting comments by only a single / character.
// This works since regular expressions are not valid JSON(5), but this will
// break if there are other valid values that begin with a / character!

			while (ch) {
				if (ch === '/') {
					comment();
				} else if (ws.indexOf(ch) >= 0) {
					next();
				} else {
					return;
				}
			}
		},

		word = function () {

// true, false, or null.

			switch (ch) {
			case 't':
				next('t');
				next('r');
				next('u');
				next('e');
				return true;
			case 'f':
				next('f');
				next('a');
				next('l');
				next('s');
				next('e');
				return false;
			case 'n':
				next('n');
				next('u');
				next('l');
				next('l');
				return null;
			case 'I':
				next('I');
				next('n');
				next('f');
				next('i');
				next('n');
				next('i');
				next('t');
				next('y');
				return Infinity;
			case 'N':
			  next( 'N' );
			  next( 'a' );
			  next( 'N' );
			  return NaN;
			}
			error("Unexpected " + renderChar(ch));
		},

		value,  // Place holder for the value function.

		array = function () {

// Parse an array value.

			var array = [];

			if (ch === '[') {
				next('[');
				white();
				while (ch) {
					if (ch === ']') {
						next(']');
						return array;   // Potentially empty array
					}
					// ES5 allows omitting elements in arrays, e.g. [,] and
					// [,null]. We don't allow this in JSON5.
					if (ch === ',') {
						error("Missing array element");
					} else {
						array.push(value());
					}
					white();
					// If there's no comma after this value, this needs to
					// be the end of the array.
					if (ch !== ',') {
						next(']');
						return array;
					}
					next(',');
					white();
				}
			}
			error("Bad array");
		},

		object = function () {

// Parse an object value.

			var key,
				object = {};

			if (ch === '{') {
				next('{');
				white();
				while (ch) {
					if (ch === '}') {
						next('}');
						return object;   // Potentially empty object
					}

					// Keys can be unquoted. If they are, they need to be
					// valid JS identifiers.
					if (ch === '"' || ch === "'") {
						key = string();
					} else {
						key = identifier();
					}

					white();
					next(':');
					object[key] = value();
					white();
					// If there's no comma after this pair, this needs to be
					// the end of the object.
					if (ch !== ',') {
						next('}');
						return object;
					}
					next(',');
					white();
				}
			}
			error("Bad object");
		};

	value = function () {

// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.

		white();
		switch (ch) {
		case '{':
			return object();
		case '[':
			return array();
		case '"':
		case "'":
			return string();
		case '-':
		case '+':
		case '.':
			return number();
		default:
			return ch >= '0' && ch <= '9' ? number() : word();
		}
	};

// Return the json_parse function. It will have access to all of the above
// functions and variables.

	return function (source, reviver) {
		var result;

		text = String(source);
		at = 0;
		lineNumber = 1;
		columnNumber = 1;
		ch = ' ';
		result = value();
		white();
		if (ch) {
			error("Syntax error");
		}

// If there is a reviver function, we recursively walk the new structure,
// passing each name/value pair to the reviver function for possible
// transformation, starting with a temporary root object that holds the result
// in an empty key. If there is not a reviver function, we simply return the
// result.

		return typeof reviver === 'function' ? (function walk(holder, key) {
			var k, v, value = holder[key];
			if (value && typeof value === 'object') {
				for (k in value) {
					if (Object.prototype.hasOwnProperty.call(value, k)) {
						v = walk(value, k);
						if (v !== undefined) {
							value[k] = v;
						} else {
							delete value[k];
						}
					}
				}
			}
			return reviver.call(holder, key, value);
		}({'': result}, '')) : result;
	};
}());

// JSON5 stringify will not quote keys where appropriate
JSON5.stringify = function (obj, replacer, space) {
	if (replacer && (typeof(replacer) !== "function" && !isArray(replacer))) {
		throw new Error('Replacer must be a function or an array');
	}
	var getReplacedValueOrUndefined = function(holder, key, isTopLevel) {
		var value = holder[key];

		// Replace the value with its toJSON value first, if possible
		if (value && value.toJSON && typeof value.toJSON === "function") {
			value = value.toJSON();
		}

		// If the user-supplied replacer if a function, call it. If it's an array, check objects' string keys for
		// presence in the array (removing the key/value pair from the resulting JSON if the key is missing).
		if (typeof(replacer) === "function") {
			return replacer.call(holder, key, value);
		} else if(replacer) {
			if (isTopLevel || isArray(holder) || replacer.indexOf(key) >= 0) {
				return value;
			} else {
				return undefined;
			}
		} else {
			return value;
		}
	};

	function isWordChar(c) {
		return (c >= 'a' && c <= 'z') ||
			(c >= 'A' && c <= 'Z') ||
			(c >= '0' && c <= '9') ||
			c === '_' || c === '$';
	}

	function isWordStart(c) {
		return (c >= 'a' && c <= 'z') ||
			(c >= 'A' && c <= 'Z') ||
			c === '_' || c === '$';
	}

	function isWord(key) {
		if (typeof key !== 'string') {
			return false;
		}
		if (!isWordStart(key[0])) {
			return false;
		}
		var i = 1, length = key.length;
		while (i < length) {
			if (!isWordChar(key[i])) {
				return false;
			}
			i++;
		}
		return true;
	}

	// export for use in tests
	JSON5.isWord = isWord;

	// polyfills
	function isArray(obj) {
		if (Array.isArray) {
			return Array.isArray(obj);
		} else {
			return Object.prototype.toString.call(obj) === '[object Array]';
		}
	}

	function isNumber(obj) {
		return Object.prototype.toString.call(obj) === '[object Number]';
	}

	function isDate(obj) {
		return Object.prototype.toString.call(obj) === '[object Date]';
	}

	var objStack = [];
	function checkForCircular(obj) {
		for (var i = 0; i < objStack.length; i++) {
			if (objStack[i] === obj) {
				throw new TypeError("Converting circular structure to JSON");
			}
		}
	}

	function makeIndent(str, num, noNewLine) {
		if (!str) {
			return "";
		}
		// indentation no more than 10 chars
		if (str.length > 10) {
			str = str.substring(0, 10);
		}

		var indent = noNewLine ? "" : "\n";
		for (var i = 0; i < num; i++) {
			indent += str;
		}

		return indent;
	}

	var indentStr;
	if (space) {
		if (typeof space === "string") {
			indentStr = space;
		} else if (typeof space === "number" && space >= 0) {
			indentStr = makeIndent(" ", space, true);
		} else {
			// ignore space parameter
		}
	}

	// Copied from Crokford's implementation of JSON
	// See https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195
	// Begin
	var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
		escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
		meta = { // table of character substitutions
		'\b': '\\b',
		'\t': '\\t',
		'\n': '\\n',
		'\f': '\\f',
		'\r': '\\r',
		'"' : '\\"',
		'\\': '\\\\'
	};
	function escapeString(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
		escapable.lastIndex = 0;
		return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
			var c = meta[a];
			return typeof c === 'string' ?
				c :
				'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
		}) + '"' : '"' + string + '"';
	}
	// End

	function internalStringify(holder, key, isTopLevel) {
		var buffer, res;

		// Replace the value, if necessary
		var obj_part = getReplacedValueOrUndefined(holder, key, isTopLevel);

		if (obj_part && !isDate(obj_part) && !isNumber(obj_part)) {
			// unbox objects
			// don't unbox dates, since will turn it into number
			obj_part = obj_part.valueOf();
		}
		switch(typeof obj_part) {
			case "boolean":
				return obj_part.toString();

			case "number":
				if (isNaN(obj_part) || !isFinite(obj_part)) {
					return "null";
				}
				return obj_part.toString();

			case "string":
				return escapeString(obj_part.toString());

			case "object":
				if (obj_part === null) {
					return "null";
				} else if (isArray(obj_part)) {
					checkForCircular(obj_part);
					buffer = "[";
					objStack.push(obj_part);

					for (var i = 0; i < obj_part.length; i++) {
						res = internalStringify(obj_part, i, false);
						buffer += makeIndent(indentStr, objStack.length);
						if (res === null || typeof res === "undefined") {
							buffer += "null";
						} else {
							buffer += res;
						}
						if (i < obj_part.length-1) {
							buffer += ",";
						} else if (indentStr) {
							buffer += "\n";
						}
					}
					objStack.pop();
					if (obj_part.length) {
						buffer += makeIndent(indentStr, objStack.length, true)
					}
					buffer += "]";
				}else if(isNumber(obj_part)) {
				  return obj_part.toString();
				} else {
					checkForCircular(obj_part);
					buffer = "{";
					var nonEmpty = false;
					objStack.push(obj_part);
					for (var prop in obj_part) {
						if (obj_part.hasOwnProperty(prop)) {
							var value = internalStringify(obj_part, prop, false);
							isTopLevel = false;
							if (typeof value !== "undefined" && value !== null) {
								buffer += makeIndent(indentStr, objStack.length);
								nonEmpty = true;
								if (JSON5.compatible) {
									key = escapeString(prop);
								}else{
									key = isWord(prop) ? prop : escapeString(prop);
								}
								
								buffer += key + ":" + (indentStr ? ' ' : '') + value + ",";
							}
						}
					}
					objStack.pop();
					if (nonEmpty) {
						buffer = buffer.substring(0, buffer.length-1) + makeIndent(indentStr, objStack.length) + "}";
					} else {
						buffer = '{}';
					}
				}
				return buffer;
			default:
				// functions and undefined should be ignored
				return undefined;
		}
	}

	// special case...when undefined is used inside of
	// a compound object/array, return null.
	// but when top-level, return undefined
	var topLevelHolder = {"":obj};
	if (obj === undefined) {
		return getReplacedValueOrUndefined(topLevelHolder, '', true);
	}
	return internalStringify(topLevelHolder, '', true);
};
